⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。
Plan Mode V2 是 CC 的”先规划、后执行”模式——将复杂任务分解为结构化计划,由多个 Agent 并行探索后再决策。
一、背景:Plan Mode 的演进
| 版本 | 特点 |
|---|---|
| Plan Mode V1 | 单 Agent 串行规划,用户审批后执行 |
| Plan Mode V2 | 多 Agent 并行探索(Explore Phase),5阶段工作流,订阅分级 |
二、核心参数配置
2.1 执行 Agent 数量
getPlanModeV2AgentCount(): number { |
| 订阅类型 | 执行 Agent 数 |
|---|---|
| Max (20x tier) | 3 |
| Enterprise / Team | 3 |
| 其他(Free/Pro) | 1 |
| 开发调试(ENV 覆盖) | 1-10 |
2.2 探索 Agent 数量
getPlanModeV2ExploreAgentCount(): number { |
探索 Agent 独立于执行 Agent:探索阶段并行跑 3 个 Sonnet 实例快速理解代码库,不受订阅限制。
三、5 阶段工作流
完整工作流定义在
messages.ts(getPlanPhase4Section 等函数),planModeV2.ts 只提供配置参数。
Phase 1: Interview(访谈阶段) |
四、Interview Phase 门控
isPlanModeInterviewPhaseEnabled(): boolean { |
Interview Phase 让模型在规划前充分理解需求,减少规划偏差。
五、Pewter Ledger 实验(计划文件格式优化)
// Feature Flag: tengu_pewter_ledger |
实验背景
| 指标 | 数值(2026-03-02, N=26.3M) |
|---|---|
| 计划文件 p50 大小 | 4,906 字符 |
| 计划文件 p90 大小 | 11,617 字符 |
| 计划文件平均大小 | 6,207 字符 |
| 模型占比 | 82% Opus 4.6 |
| 拒绝率(<2K chars) | 20% |
| 拒绝率(>20K chars) | 50% |
实验目标
主指标:session-level Avg Cost(Opus 输出是输入价格的 5×,成本是输出量的加权代理)
护栏指标:
feedback-bad rate(计划太薄 → 实现迭代更多 → 更多工具调用)requests/session(反映计划是否导致更多来回)tool error rate
三种实验臂
| 臂 | 含义 | 目的 |
|---|---|---|
trim |
轻度缩减计划文件大小指导 | 温和约束 |
cut |
中度缩减 | 中等约束 |
cap |
严格上限 | 强约束 |
null |
控制组,无约束 | 基准 |
六、与 Swarm 系统的关系
Plan Mode V2 的多 Agent 执行本质上复用了 Swarm 基础设施:
PlanModeV2 执行阶段 |
差异:Plan Mode 的 SubAgent 是有计划引导的(知道整体方案),Swarm 的 Teammate 更独立。
七、Feature Flag 体系总结
| Flag | 控制内容 | 默认值 |
|---|---|---|
tengu_plan_mode_interview_phase |
是否启用访谈阶段 | false(外部用户) |
tengu_pewter_ledger |
计划文件格式约束级别 | null(控制组) |
CLAUDE_CODE_PLAN_V2_AGENT_COUNT |
执行 Agent 数(ENV 覆盖) | 订阅决定 |
CLAUDE_CODE_PLAN_V2_EXPLORE_AGENT_COUNT |
探索 Agent 数(ENV 覆盖) | 3 |
八、面试要点
Q:Plan Mode V2 与 V1 的核心区别?
V2 引入了并行 Explore Phase(3 个 Agent 并发探索代码库),将”理解问题”与”执行任务”分开。V1 是单 Agent 串行:规划 → 用户批准 → 执行。V2 让规划更深入,执行更并行。
Q:为什么 Explore Agent 数量不受订阅限制,但执行 Agent 受限?
Explore Agent 只读不写,成本可预估且较低(快速读代码)。执行 Agent 涉及写文件、运行命令,并行越多风险和成本越高,因此通过订阅分级控制。
Q:Pewter Ledger 实验为什么以 Avg Cost 而非 planLengthChars 为主指标?
更短的计划文件可能反而增加”写→计数→编辑”循环次数,导致总输出 token 更多。主指标直接量化最终成本,而不是代理指标(计划长度),避免局部优化陷阱。
九、Plan Mode 状态机与主循环集成(深度解析)
9.1 权限模式状态定义
Plan Mode 本质上是 CC 权限系统中的一个模式(mode),由 toolPermissionContext.mode 字段控制。合法值为:
'default' | 'auto' | 'plan' | 'acceptEdits' | 'bypassPermissions' |
Plan Mode 对应 'plan',进入时还会保存上一个模式到 prePlanMode,退出时用于恢复。
状态转移路径:
default / auto |
9.2 进入 Plan Mode 的状态写入
EnterPlanModeTool.call() 的核心逻辑:
// EnterPlanModeTool.ts - call() |
prepareContextForPlanMode() 负责在切换到 plan 模式前的准备工作(如激活分类器副作用),applyPermissionUpdate 执行实际的 mode 写入并将当前 mode 存入 prePlanMode。
9.3 Plan Mode 中的工具禁用机制
Plan Mode 激活时,模型只能使用只读工具(Glob、Grep、Read、LS)加上 AskUserQuestion。写文件/执行命令类工具在权限检查阶段被拒绝。
关键约束来自两个层面:
- Prompt 层约束(软约束):
mapToolResultToToolResultBlockParam在进入成功的 tool_result 中明确写入:
// EnterPlanModeTool.ts - mapToolResultToToolResultBlockParam() |
- 权限层约束(硬约束):
mode: 'plan'状态下,权限系统对写操作返回deny,模型无论怎样尝试调用 Bash/FileWrite 都会被拒绝,不会弹出用户确认框。
9.4 shouldDefer: true 与主循环集成
EnterPlanModeTool 和 ExitPlanModeV2Tool 都设置了 shouldDefer: true,表示这两个工具需要暂停当前 query loop、等待用户交互后再继续:
shouldDefer: true // 两个工具均设置 |
主循环(query loop)遇到 deferred tool 时会:
- 暂停推进 assistant turn
- 把控制权交给 UI permission dialog
- 用户选择后,把结果作为 tool_result 注入,继续 loop
这正是 Plan Mode 不需要 model 在 tool_use 里带计划内容的原因——整个”弹窗 → 用户确认 → 模式切换”是同步阻塞流程。
十、EnterPlanModeTool 实现详解
10.1 工具元数据
// constants.ts |
输入 schema 为空对象(不需要任何参数):
const inputSchema = lazySchema(() => z.strictObject({})) |
10.2 触发条件(外部用户 vs 内部用户的差异)
prompt.ts 根据 USER_TYPE 环境变量分发两套 prompt:
外部用户(getEnterPlanModeToolPromptExternal)——更激进地触发:
- 任何新功能实现
- 多文件修改(> 2-3 个文件)
- 有多个合理方案的任务
- 需求不清晰时
- 用户偏好可能影响实现方式时
核心指导语:
“Use this tool proactively when you’re about to start a non-trivial implementation task.”
内部用户(getEnterPlanModeToolPromptAnt)——更保守,避免过度规划:
- 仅当有显著架构歧义时
- 需求真正不清晰时
- 高影响重构时
- 明确排除:用户说”can we work on X”时直接开始,不进规划
核心指导语:
“When in doubt, prefer starting work and using AskUserQuestion for specific questions over entering a full planning phase.”
这个差异反映了 CC 团队的实际经验:内部用户(ant)对代码库更熟悉,过度规划反而是摩擦。
10.3 KAIROS 渠道禁用保护
isEnabled() { |
当 CC 运行在 Telegram/Discord 等渠道模式时(KAIROS),EnterPlanMode 和 ExitPlanMode 同时禁用。原因:ExitPlanMode 的审批 dialog 需要 TUI 终端,渠道模式下没有 TUI,进入后无法退出,形成死锁。
10.4 EnterPlanMode 不能在 Agent 上下文中调用
async call(_input, context) { |
Plan Mode 是顶层 session 级别的模式切换,SubAgent 不能触发它。这防止了 SubAgent 意外改变顶层模式。
十一、用户确认 UI 流程(EnterPlanModePermissionRequest)
11.1 组件触发路径
model 发出 tool_use: EnterPlanMode |
11.2 Dialog 显示内容
╔════════════════════════════════╗ |
11.3 用户选择处理(handleResponse)
// EnterPlanModePermissionRequest.tsx |
Yes 路径:
- 记录
tengu_plan_enteranalytics 事件(含 interviewPhase 是否开启) - 调用
handlePlanModeTransition处理模式转换副作用 - 调用
toolUseConfirm.onAllow传入setMode: 'plan'权限更新 - 主循环收到 allow + 权限更新,将 mode 写入 AppState
No 路径:
- 调用
toolUseConfirm.onReject() - 工具调用被拒绝,model 收到 rejection,可以直接开始编码
十二、ExitPlanModeV2Tool 实现详解
12.1 工具输入 Schema
const inputSchema = lazySchema(() => |
allowedPrompts 是模型在退出规划阶段时请求的语义级 Bash 权限——描述”需要做什么”而非具体命令,权限系统根据此做模式匹配授权。
注意:Plan 内容本身不在输入 schema 里,由 normalizeToolInput 从磁盘读取后注入(详见 12.4)。
12.2 输出 Schema
z.object({ |
12.3 validateInput 前置校验
async validateInput(_input, { getAppState, options }) { |
防止非 plan 模式下调用 ExitPlanMode:compact/clear 后 deferred tool 列表里仍有 ExitPlanMode,model 可能在非 plan 状态下错误调用。validateInput 在 checkPermissions(会弹用户 dialog)之前拦截,避免给用户弹出无意义的审批窗口,同时记录异常事件。
12.4 Plan 文件读取链路
Plan 内容从不在网络调用中传递,而是走文件系统:
model 规划阶段写文件(FileWrite 工具写 plan file) |
CCR Web UI 的特殊路径:用户可以在 Web UI 中编辑计划文本,编辑后的版本通过 permissionResult.updatedInput.plan 传入,call() 优先使用此版本并写回磁盘:
const inputPlan = |
12.5 退出时的权限恢复
context.setAppState(prev => { |
退出逻辑处理三种情形:
| 进入前模式 | auto gate 状态 | 退出后模式 |
|---|---|---|
default |
任意 | default |
auto |
gate 开启 | auto(恢复) |
auto |
gate 关闭(断路) | default(安全回退) |
十三、Plan 文件的存储与生命周期
13.1 文件路径生成
// plans.ts |
planSlug 是基于当前 session ID 生成的随机词组(如 violet-mountain),保证在 plans 目录下唯一(最多重试 10 次)。
默认存储位置:~/.claude/plans/{slug}.md(可在 settings.json 的 plansDirectory 中自定义,但必须在项目根目录内)。
13.2 计划恢复机制(Resume & Fork)
CC 支持会话恢复(/resume)和会话 fork(Ctrl+G),plan 文件需要跨会话持久化。
Resume 场景:copyPlanForResume() 从 log 中读取原始 slug,恢复到同一文件路径。在 CCR(远程)环境中,文件不持久,优先从 file_snapshot 系统消息恢复,其次从消息历史中的三种位置提取:
// plans.ts - recoverPlanFromMessages() |
Fork 场景:copyPlanForFork() 生成新 slug,将原计划内容复制到新文件,防止原会话和 fork 会话互相覆盖。
13.3 远程环境文件快照
CCR(Claude Code Remote)无本地文件系统,计划文件通过增量快照嵌入 transcript:
// plans.ts - persistFileSnapshotIfRemote() |
每次 writeFile 到 plan 文件后都会触发快照,保证远程会话的计划内容不会丢失。
十四、Teammate 模式下的 Plan 审批流
当 ExitPlanMode 在 Teammate(SubAgent)上下文中被调用时,走完全不同的审批路径:
if (isTeammate() && isPlanModeRequired()) { |
Tool result 告知 Teammate:
Your plan has been submitted to the team lead for approval. |
Teammate 进入等待状态,Team Lead 在 UI 中审批后,通过 mailbox 协议通知 Teammate 继续。
十五、面试深度题(含 V2 vs V1 对比)
Q1:Plan Mode V2 相比 V1 最核心的改进是什么?从源码角度说明。
V1 是单 Agent 串行流程:EnterPlanMode → 探索 → ExitPlanMode → 用户批准 → 执行。V2 有三处关键改进:
并行 Explore Phase:
getPlanModeV2ExploreAgentCount()固定返回 3,所有订阅等级都获得 3 个并行探索 Agent,分别探索代码库的不同部分,汇总后给主 Agent 更丰富的上下文。执行并行化:
getPlanModeV2AgentCount()根据订阅返回 1-3,Max/Enterprise/Team 用户进入执行阶段时可并行跑多个实现 Agent,将大型计划的各子任务同时推进。Interview Phase(可选):
isPlanModeInterviewPhaseEnabled()控制,内部用户始终开启——在探索前先通过多轮对话彻底澄清需求,减少”探索方向偏了”的浪费。
Q2:Plan Mode 如何在技术层面防止 model 在规划阶段”偷跑”工具(写文件、执行命令)?
双重防线:
第一层(软约束/Prompt 层):
mapToolResultToToolResultBlockParam在 Enter 成功的 tool_result 中注入明确指令:"DO NOT write or edit any files yet. This is a read-only exploration and planning phase."这段文字直接出现在 context window 里,作为强约束。第二层(硬约束/权限层):
mode: 'plan'状态下,权限系统对所有写操作统一返回deny,不弹用户确认。即使 model 无视 prompt 指令尝试调用 Bash 或 FileWrite,也会在checkPermissions阶段被拒绝,工具调用失败,不产生任何副作用。两层保护互补:prompt 层减少模型的主动尝试,权限层提供安全网。
Q3:ExitPlanMode 的 validateInput 为什么要在 checkPermissions 之前拦截”非 plan 模式调用”?
checkPermissions在验证通过后会向用户显示确认 dialog(弹窗),如果 model 在非 plan 模式下(如 compact 后)错误地调用 ExitPlanMode,让用户看到”退出规划模式?”的弹窗会造成困惑。validateInput先于权限检查运行,直接返回{ result: false, message: '...' },主循环将其转化为工具错误注入 context,model 自行纠正,用户完全无感知。同时记录tengu_exit_plan_mode_called_outside_plan事件,供团队监控此类异常频率。
Q4:Plan 文件的内容如何从规划阶段传递到执行阶段,以及 CCR 远程环境如何保证不丢失?
本地环境下,plan 文件写在
~/.claude/plans/{slug}.md,整个 session 期间文件持久存在,getPlan()随时可读。normalizeToolInput在 ExitPlanMode 被调用时从磁盘读取内容注入input.plan,用户批准后,内容随 tool_result"## Approved Plan:\n{plan}"进入 context window,实现从规划阶段到执行阶段的信息传递。CCR(远程)环境无持久文件系统,通过两个机制保证不丢失:
- 增量快照:每次写 plan 文件后调用
persistFileSnapshotIfRemote(),将内容序列化为SystemFileSnapshotMessage追加到 transcript。- Resume 恢复:
copyPlanForResume()在恢复会话时,先找 file_snapshot,再扫描消息历史中 ExitPlanMode tool_use 的input.plan、user message 的planContent、attachment 的plan_file_reference,三路回退确保计划内容可靠恢复。
涉及源文件
src/utils/planModeV2.ts


